3. Transformação de dados#


Nos capítulos anteriores, aprendemos como usar marcas e codificações visuais para representar registros de dados individuais. Aqui, exploraremos métodos de transformação de dados, incluindo o uso de agregações para resumir múltiplos registros. A transformação de dados é uma parte integral da visualização: escolher quais variáveis exibir, bem como seu nível de detalhamento, é tão importante quanto escolher as codificações visuais apropriadas. Afinal, não importa o quão bem escolhidas sejam suas codificações visuais se você estiver mostrando as informações erradas!

Enquanto percorre este módulo, recomendamos que você abra a documentação de Transformações de Dados do Altair em outra aba. Será um recurso útil caso, em algum momento, você queira mais detalhes ou deseje ver quais outras transformações estão disponíveis.

Este capítulo faz parte do currículo de visualização de dados.

import pandas as pd
import altair as alt

3.1 Dados sobre Filmes#


from vega_datasets import data
movies_url = data.movies()
movies_url
Title US_Gross Worldwide_Gross US_DVD_Sales Production_Budget Release_Date MPAA_Rating Running_Time_min Distributor Source Major_Genre Creative_Type Director Rotten_Tomatoes_Rating IMDB_Rating IMDB_Votes
0 The Land Girls 146083.0 146083.0 NaN 8000000.0 Jun 12 1998 R NaN Gramercy None None None None NaN 6.1 1071.0
1 First Love, Last Rites 10876.0 10876.0 NaN 300000.0 Aug 07 1998 R NaN Strand None Drama None None NaN 6.9 207.0
2 I Married a Strange Person 203134.0 203134.0 NaN 250000.0 Aug 28 1998 None NaN Lionsgate None Comedy None None NaN 6.8 865.0
3 Let's Talk About Sex 373615.0 373615.0 NaN 300000.0 Sep 11 1998 None NaN Fine Line None Comedy None None 13.0 NaN NaN
4 Slam 1009819.0 1087521.0 NaN 1000000.0 Oct 09 1998 R NaN Trimark Original Screenplay Drama Contemporary Fiction None 62.0 3.4 165.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
3196 Zack and Miri Make a Porno 31452765.0 36851125.0 21240321.0 24000000.0 Oct 31 2008 R 101.0 Weinstein Co. Original Screenplay Comedy Contemporary Fiction Kevin Smith 65.0 7.0 55687.0
3197 Zodiac 33080084.0 83080084.0 20983030.0 85000000.0 Mar 02 2007 R 157.0 Paramount Pictures Based on Book/Short Story Thriller/Suspense Dramatization David Fincher 89.0 NaN NaN
3198 Zoom 11989328.0 12506188.0 6679409.0 35000000.0 Aug 11 2006 PG NaN Sony Pictures Based on Comic/Graphic Novel Adventure Super Hero Peter Hewitt 3.0 3.4 7424.0
3199 The Legend of Zorro 45575336.0 141475336.0 NaN 80000000.0 Oct 28 2005 PG 129.0 Sony Pictures Remake Adventure Historical Fiction Martin Campbell 26.0 5.7 21161.0
3200 The Mask of Zorro 93828745.0 233700000.0 NaN 65000000.0 Jul 17 1998 PG-13 136.0 Sony Pictures Remake Adventure Historical Fiction Martin Campbell 82.0 6.7 4789.0

3201 rows × 16 columns

3.2 Histogramas#


Começaremos nosso tour de transformação agrupando os dados em categorias discretas e contando os registros para resumir esses grupos. Os gráficos resultantes são conhecidos como histogramas.

Primeiro, vamos observar os dados não agregados: um gráfico de dispersão que mostra as avaliações de filmes do Rotten Tomatoes em comparação com as avaliações dos usuários do IMDB. Forneceremos os dados ao Altair passando a URL dos dados dos filmes para o método Chart. (Também poderíamos passar diretamente um DataFrame do Pandas para obter o mesmo resultado.) Em seguida, podemos codificar os campos de avaliação do Rotten Tomatoes e do IMDB usando os canais x e y:

alt.Chart(movies_url).mark_circle().encode(
    alt.X('Rotten_Tomatoes_Rating:Q'),
    alt.Y('IMDB_Rating:Q')
)

Para resumir esses dados, podemos agrupar um campo de dados para agrupar valores numéricos em grupos discretos. Aqui, agrupamos ao longo do eixo x adicionando bin=True ao canal de codificação x. O resultado é um conjunto de dez intervalos de mesmo tamanho, cada um correspondendo a um intervalo de dez pontos de avaliação.

alt.Chart(movies_url).mark_circle().encode(
    alt.X('Rotten_Tomatoes_Rating:Q', bin=True),
    alt.Y('IMDB_Rating:Q')
)

Definir bin=True usa as configurações padrão de agrupamento, mas podemos ter mais controle, se desejado. Em vez disso, vamos definir o número máximo de intervalos (maxbins) como 20, o que tem o efeito de dobrar a quantidade de grupos. Agora, cada intervalo corresponde a um espaço de cinco pontos de avaliação.

alt.Chart(movies_url).mark_circle().encode(
    alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
    alt.Y('IMDB_Rating:Q')
)

Com os dados agrupados, vamos agora resumir a distribuição das avaliações do Rotten Tomatoes. Por enquanto, removeremos as avaliações do IMDB e, em vez disso, usaremos o canal de codificação y para mostrar uma contagem agregada de registros (count), de modo que a posição vertical de cada ponto indique o número de filmes em cada intervalo de avaliação do Rotten Tomatoes.

Como a agregação count conta o número total de registros em cada intervalo independentemente dos valores dos campos, não precisamos incluir um nome de campo na codificação y.

alt.Chart(movies_url).mark_circle().encode(
    alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
    alt.Y('count()')
)

Para chegarmos a um histograma padrão, vamos mudar o tipo de marca de circle para bar:

alt.Chart(movies_url).mark_bar().encode(
    alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
    alt.Y('count()')
)

Agora podemos examinar a distribuição das avaliações com mais clareza: vemos menos filmes na extremidade negativa e um pouco mais na extremidade alta, mas, no geral, a distribuição é relativamente uniforme. As avaliações do Rotten Tomatoes são determinadas com base nos julgamentos de críticos de cinema, classificando os filmes como “positivo” ou “negativo” e calculando a porcentagem de críticas positivas. Parece que essa abordagem faz um bom trabalho ao utilizar toda a faixa de valores de avaliação.

Da mesma forma, podemos criar um histograma para as avaliações do IMDB apenas alterando o campo no canal de codificação x:

alt.Chart(movies_url).mark_bar().encode(
    alt.X('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
    alt.Y('count()')
)

Em contraste com a distribuição mais uniforme que vimos antes, as avaliações do IMDB exibem uma distribuição em forma de sino (embora negativamente assimétrica). As avaliações do IMDB são calculadas a partir da média das notas (variando de 1 a 10) fornecidas pelos usuários do site. Podemos perceber que essa forma de medição resulta em uma distribuição diferente das avaliações do Rotten Tomatoes. Também podemos observar que a moda da distribuição está entre 6,5 e 7: as pessoas geralmente gostam de assistir a filmes, o que pode explicar esse viés positivo!

Agora, vamos voltar ao nosso gráfico de dispersão das avaliações do Rotten Tomatoes e do IMDB. Veja o que acontece se agruparmos ambos os eixos do nosso gráfico original.

alt.Chart(movies_url).mark_circle().encode(
    alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
    alt.Y('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
)

Detalhes são perdidos devido ao excesso de sobreposição, com muitos pontos desenhados diretamente uns sobre os outros.

Para formar um histograma bidimensional, podemos adicionar uma contagem agregada (count), como fizemos antes. Como os canais de codificação x e y já estão ocupados, devemos usar um canal de codificação diferente para representar as contagens. Aqui está o resultado de usar a área dos círculos adicionando um canal de codificação de tamanho.

alt.Chart(movies_url).mark_circle().encode(
    alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
    alt.Y('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
    alt.Size('count()')
)

Alternativamente, podemos representar as contagens usando o canal de cor (color) e alterar o tipo de marca para barra (bar). O resultado é um histograma bidimensional no formato de um mapa de calor.

alt.Chart(movies_url).mark_bar().encode(
    alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
    alt.Y('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
    alt.Color('count()')
)

Compare os histogramas 2D baseados em tamanho e cor acima. Qual codificação você acha que deve ser preferida? Por quê? Em qual gráfico você consegue comparar com mais precisão a magnitude dos valores individuais? Em qual gráfico você consegue visualizar com mais precisão a densidade geral das avaliações?

3.3 Agregação#


Contagens são apenas um tipo de agregado. Também podemos calcular resumos usando medidas como average, median, min ou max. A documentação do Altair inclui o conjunto completo de funções de agregação disponíveis.

Vamos dar uma olhada em alguns exemplos!

3.3.1 Médias e classificação#

Diferentes gêneros de filmes recebem classificações consistentemente diferentes dos críticos? Como um primeiro passo para responder a essa pergunta, podemos examinar a classificação média (também conhecida como média aritmética) para cada gênero de filme.

Vamos visualizar o gênero ao longo do eixo y e plotar as classificações médias do Rotten Tomatoes ao longo do eixo x.

alt.Chart(movies_url).mark_bar().encode(
    alt.X('average(Rotten_Tomatoes_Rating):Q'),
    alt.Y('Major_Genre:N')
)

Parece haver alguma variação interessante, mas olhar os dados como uma lista alfabética não é muito útil para classificar reações críticas aos gêneros.

Para uma imagem mais organizada, vamos classificar os gêneros em ordem decrescente de classificação média. Para fazer isso, adicionaremos um parâmetro sort ao canal de codificação y, declarando que desejamos classificar pela classificação do Rotten Tomatoes média (op, a operação agregada) (o campo) em ordem decrescente.

alt.Chart(movies_url).mark_bar().encode(
    alt.X('average(Rotten_Tomatoes_Rating):Q'),
    alt.Y('Major_Genre:N', sort=alt.EncodingSortField(
        op='average', field='Rotten_Tomatoes_Rating', order='descending')
    )
)

O enredo ordenado sugere que os críticos têm em alta conta documentários, musicais, faroestes e dramas, mas menosprezam comédias românticas e filmes de terror… e quem não gosta de filmes nulos!?

3.3.2 Medianas e o intervalo interquartil#

Embora as médias sejam uma forma comum de resumir dados, elas podem, às vezes, enganar. Por exemplo, valores muito grandes ou muito pequenos (outliers) podem distorcer a média. Para ficar seguro, podemos comparar os gêneros de acordo com as classificações mediana também.

A mediana é um ponto que divide os dados uniformemente, de modo que metade dos valores são menores que a mediana e a outra metade é maior. A mediana é menos sensível a outliers e, portanto, é chamada de estatística robusta. Por exemplo, aumentar arbitrariamente o maior valor de classificação não fará com que a mediana mude.

Vamos atualizar nosso gráfico para usar um agregado median e classificar por esses valores:

alt.Chart(movies_url).mark_bar().encode(
    alt.X('median(Rotten_Tomatoes_Rating):Q'),
    alt.Y('Major_Genre:N', sort=alt.EncodingSortField(
        op='median', field='Rotten_Tomatoes_Rating', order='descending')
    )
)

Podemos ver que alguns dos gêneros com médias semelhantes trocaram de lugar (filmes de gênero desconhecido, ou nulos, agora são classificados como os mais altos!), mas os grupos gerais permaneceram estáveis. Filmes de terror continuam a receber pouco amor dos críticos profissionais de cinema.

É uma boa ideia permanecer cético ao visualizar estatísticas agregadas. Até agora, olhamos apenas para estimativas pontuais. Não examinamos como as classificações variam dentro de um gênero.

Vamos visualizar a variação entre as classificações para adicionar alguma nuance às nossas classificações. Aqui, codificaremos o intervalo interquartil (IQR) para cada gênero. O IQR é o intervalo no qual a metade do meio dos valores de dados reside. Um quartil contém 25% dos valores de dados. O intervalo interquartil consiste nos dois quartis do meio e, portanto, contém os 50% do meio.

Para visualizar intervalos, podemos usar os canais de codificação x e x2 para indicar os pontos inicial e final. Usamos as funções agregadas q1 (o limite do quartil inferior) e q3 (o limite do quartil superior) para fornecer o intervalo interquartil. (Caso você esteja se perguntando, q2 seria a mediana.)

alt.Chart(movies_url).mark_bar().encode(
    alt.X('q1(Rotten_Tomatoes_Rating):Q'),
    alt.X2('q3(Rotten_Tomatoes_Rating):Q'),
    alt.Y('Major_Genre:N', sort=alt.EncodingSortField(
        op='median', field='Rotten_Tomatoes_Rating', order='descending')
    )
)

3.3.3 Unidades de tempo#

Agora, vamos fazer uma pergunta completamente diferente: as bilheteiras variam conforme a temporada?

Para obter uma resposta inicial, vamos traçar um gráfico da mediana da receita bruta nos EUA por mês.

Para criar esse gráfico, utilizaremos a transformação timeUnit para mapear as datas de lançamento para o (month) mês do ano. O resultado é semelhante ao agrupamento, mas usa intervalos de tempo significativos. Outras unidades de tempo válidas incluem: year, quarter, date (dia numérico do mês), day (dia da semana) e hours, além de unidades compostas, como yearmonth ou hoursminutes. Consulte a documentação do Altair para ver a lista completa de unidades de tempo disponíveis.

alt.Chart(movies_url).mark_area().encode(
    alt.X('month(Release_Date):T'),
    alt.Y('median(US_Gross):Q')
)

Observando o gráfico resultante, as vendas medianas de filmes nos EUA parecem aumentar durante a temporada de blockbusters no verão e no período de férias de fim de ano. É claro que pessoas ao redor do mundo (não apenas nos EUA) vão ao cinema. Um padrão semelhante ocorre na receita bruta mundial?

alt.Chart(movies_url).mark_area().encode(
    alt.X('month(Release_Date):T'),
    alt.Y('median(Worldwide_Gross):Q')
)

Sim!

3.4 Técnicas Avançadas de Transformação de Dados#


Todos os exemplos acima usam transformações (bin, timeUnit, aggregate, sort) que são definidas relativo a um canal de codifcação. Contudo, as vezes você quer aplicar uma cadeia de transformações antes da visualização, ou talvez usar transformações que não integram nas definições de codificação do Altair. Para esses casos específicos, o Altair e o Vega-Lite disponibiliza transformações de dados que estão definidas separadamente das codificações. Essas transformações então serão aplicadas aos dados antes de quaisquer codifcações forem aplicadas.

Poderiamos também fazer transformações usando o Pandas diretamente, e depois visualizar o resultado. Entretanto, usar as transformações já embutidas no Altair permite nossas visualizações serem publicadas mais facilmente em alguns contextos; por exemplo, exportar o arquivo Vega-Lite em JSON para usar em alguma aplicação no navegador web. Vamos dar uma olhada nessas transformações que o Altair permite, como o calculate, filter, aggregate, e window.

3.4.1 Calcular#

Você se lembra da nossa comparação da arrecadação nos Estados Unidos com a arrecadação mundial? Teoricamente, a arrecadação mundial não considera também a arrecadação dos Estados Unidos? (realmente considera) Como podemos ter uma ideia das tendências fora dos EUA?

Com a transformação calculate podemos criar novos conjuntos de dados. Aqui queremos subtrair a arrecadação dos Estados Unidos da arrecadação mundial. A transformação pega uma expressão em string do Vega para definir a fórmula durante um unico recorte. Expressões no Vega usam a sintaxe do Javascript. O prefixo datum. acessa o conjunto com o input dado.

alt.Chart(movies_url).mark_area().transform_calculate(
    NonUS_Gross='datum.Worldwide_Gross - datum.US_Gross'
).encode(
    alt.X('month(Release_Date):T'),
    alt.Y('median(NonUS_Gross):Q')
)

Podemos ver que as tendências sazonais existem fora dos EUA, porém com um declive maior nos demais meses.

3.4.2 Filtrar#

A transformação filter cria uma nova tabela com um subconjunto dos dados originais, removendo linhas que falham em um teste de predicado fornecido. Similar à transformação calculate, predicados de filtro são expressos usando a linguagem de expressão Vega.

Abaixo adicionamos um filtro para limitar nosso gráfico de dispersão (scatter plot) inicial das avaliações do IMDB vs. Rotten Tomatoes para somente filmes com gênero principal “Romântic Comedy”.

alt.Chart(movies_url).mark_circle().encode(
    alt.X('Rotten_Tomatoes_Rating:Q'),
    alt.Y('IMDB_Rating:Q')
).transform_filter('datum.Major_Genre == "Romantic Comedy"')

Como o gráfico muda se filtrarmos para ver outros gêneros? Edite a expressão de filtro para descobrir.

Agora vamos filtrar para ver filmes lançados antes de 1970.

alt.Chart(movies_url).mark_circle().encode(
    alt.X('Rotten_Tomatoes_Rating:Q'),
    alt.Y('IMDB_Rating:Q')
).transform_filter('year(datum.Release_Date) < 1970')

Eles parecem pontuar excepcionalmente bem! Filmes mais velhos são simplesmente melhores, ou há um viés de seleção apontando para mais filmes antigos bem avaliados nesse conjunto de dados?

3.4.3 Agregar#

Nós já vimos transformações aggregate tal como count e average no contexto de canais de codificação. Nós também podemos especificar agregados separadamente, como um passo de pré-processamento para outras transformações (como nos exemplos de transformação window abaixo). A saída de uma transformação aggregate é uma nova tabela de dados com registros que contém os campos groupby e as medidas aggregate computadas.

Vamos recriar nosso gráfico de avaliação média por gênero, mas dessa vez usando uma transformação aggregate separada. A tabela de saída da transformação aggregate contém 13 linhas, uma para cada gênero.

Para ordenar o eixo Y, devemos incluir uma operação aggregate separada nas nossas instruções de ordenação. Aqui nós usamos o operador max, que funciona bem porque há somente um registro de saída por gênero. Similarmente, nós poderíamos ter usado o operador min e ter acabado com o mesmo gráfico.

alt.Chart(movies_url).mark_bar().transform_aggregate(
    groupby=['Major_Genre'],
    Average_Rating='average(Rotten_Tomatoes_Rating)'
).encode(
    alt.X('Average_Rating:Q'),
    alt.Y('Major_Genre:N', sort=alt.EncodingSortField(
        op='max', field='Average_Rating', order='descending'
      )
    )
)

3.4.4 Janela#

A transformação window realiza cálculos sobre grupos classificados de registros de dados. As transformações de janela são bastante poderosas, suportando tarefas como classificação, análise de lead/lag, totais cumulativos e somas ou médias correntes. Os valores calculados por uma transformação window são gravados de volta na tabela de dados de entrada como novos campos. As operações de janela incluem as operações agregadas que vimos anteriormente, bem como operações especializadas como rank, row_number, lead e lag. A documentação do Vega-Lite lista todas as operações válidas de janela.

Um caso de uso para uma transformação window é calcular listas top-k. Vamos plotar os 20 principais diretores em termos de total bruto mundial.

Primeiro usamos uma transformação filter para remover registros para os quais não conhecemos o diretor. Caso contrário, o diretor null dominaria a lista! Em seguida, aplicamos um aggregate para somar a receita bruta mundial de todos os filmes, agrupados por diretor. Neste ponto, poderíamos traçar um gráfico de barras classificado (sorted bar chart), mas acabaríamos com centenas e centenas de diretores. Como podemos limitar a exibição aos 20 primeiros?

A transformação window nos permite determinar os principais diretores calculando sua ordem de classificação. Dentro da nossa definição de transformação window, podemos sort por receita bruta e usar a operação rank para calcular as pontuações de classificação de acordo com essa ordem de classificação. Podemos então adicionar uma transformação filter subsequente para limitar os dados a apenas registros com um valor de classificação menor ou igual a 20.

alt.Chart(movies_url).mark_bar().transform_filter(
    'datum.Director != null'
).transform_aggregate(
    Gross='sum(Worldwide_Gross)',
    groupby=['Director']
).transform_window(
    Rank='rank()',
    sort=[alt.SortField('Gross', order='descending')]
).transform_filter(
    'datum.Rank < 20'
).encode(
    alt.X('Gross:Q'),
    alt.Y('Director:N', sort=alt.EncodingSortField(
        op='max', field='Gross', order='descending'
    ))
)

Podemos ver que Steven Spielberg tem sido bem-sucedido em sua carreira! No entanto, mostrar somas pode favorecer diretores que tiveram carreiras mais longas e, portanto, fizeram mais filmes e, portanto, mais dinheiro. O que acontece se mudarmos a escolha da operação agregada? Quem é o diretor mais bem-sucedido em termos de média ou mediana bruta por filme? Modifique a transformação agregada acima!

Anteriormente neste capítulo, examinamos histogramas, que aproximam a função de densidade de probabilidade de um conjunto de valores. Uma abordagem complementar é observar a distribuição cumulativa. Por exemplo, pense em um histograma no qual cada bin inclui não apenas sua própria contagem, mas também as contagens de todos os bins anteriores — o resultado é um total corrente, com o último bin contendo o número total de registros. Um gráfico cumulativo nos mostra diretamente, para um dado valor de referência, quantos valores de dados são menores ou iguais a essa referência.

Como um exemplo concreto, vamos olhar para a distribuição cumulativa de filmes por tempo de execução (em minutos). Apenas um subconjunto de registros realmente inclui informações de tempo de execução, então primeiro filtramos para baixo para o subconjunto de filmes para os quais temos tempos de execução. Em seguida, aplicamos um aggregate para contar o número de filmes por duração (implicitamente usando “bins” de 1 minuto cada). Em seguida, usamos uma transformação window para calcular um total contínuo de contagens em bins, classificados por tempo de execução crescente.

alt.Chart(movies_url).mark_line(interpolate='step-before').transform_filter(
    'datum.Running_Time_min != null'
).transform_aggregate(
    groupby=['Running_Time_min'],
    Count='count()',
).transform_window(
    Cumulative_Sum='sum(Count)',
    sort=[alt.SortField('Running_Time_min', order='ascending')]
).encode(
    alt.X('Running_Time_min:Q', axis=alt.Axis(title='Duration (min)')),
    alt.Y('Cumulative_Sum:Q', axis=alt.Axis(title='Cumulative Count of Films'))
)

Vamos examinar a distribuição cumulativa de durações de filmes. Podemos ver que filmes com menos de 110 minutos compõem cerca de metade de todos os filmes para os quais temos tempos de execução. Vemos um acúmulo constante de filmes entre 90 minutos e 2 horas, após o qual a distribuição começa a diminuir. Embora raro, o conjunto de dados contém vários filmes com mais de 3 horas de duração!

3.5 Resumo#


Nós apenas arranhamos a superfície do que as transformações de dados podem fazer! Para mais detalhes, incluindo todas as transformações disponíveis e seus parâmetros, veja a documentação de transformação de dados do Altair.

Às vezes, você precisará executar uma transformação de dados significativa para preparar seus dados antes de usar ferramentas de visualização. Para se envolver em disputa de dados aqui mesmo no Python, você pode usar a biblioteca Pandas.